[id].vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <!--
  2. * @Author: LiZhiWei
  3. * @Date: 2026-01-22 14:08:24
  4. * @LastEditors: LiZhiWei
  5. * @LastEditTime: 2026-01-26 10:28:46
  6. * @Description: 解决方案详情页
  7. -->
  8. <template>
  9. <div v-if="solution" class="solution-page">
  10. <!-- Hero Section -->
  11. <section class="solution-hero">
  12. <div
  13. class="landing-container flex flex-col items-center justify-center h-full text-center lt-sm:px-32px"
  14. >
  15. <h1 class="hero-title pf-sc-semibold">{{ solution.title }}</h1>
  16. <p class="hero-subtitle pf-sc-semibold">{{ solution.subTitle }}</p>
  17. <p class="hero-desc pf-sc-regular">{{ solution.description }}</p>
  18. <button class="hero-btn btn-primary pf-sc-semibold" @click="openConsultation">
  19. 立即免费咨询
  20. </button>
  21. </div>
  22. </section>
  23. <!-- Core Pain Points -->
  24. <section
  25. ref="painPointsRef"
  26. class="section-container bg-white transition-all duration-1000 ease-out"
  27. :class="[isPainPointsVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-50px']"
  28. >
  29. <div class="landing-container lt-sm:px-32px">
  30. <h2 class="section-title pf-sc-semibold">{{ solution.corePoints.title }}</h2>
  31. <div class="grid grid-cols-3 gap-25px lt-sm:grid-cols-1">
  32. <div
  33. v-for="(item, index) in solution.corePoints.points"
  34. :key="index"
  35. class="pain-card group"
  36. >
  37. <i :class="[item.icon, 'pain-icon']"></i>
  38. <h3 class="pain-title pf-sc-semibold">{{ item.point }}</h3>
  39. <p class="pain-desc pf-sc-regular">{{ item.pointDesc }}</p>
  40. </div>
  41. </div>
  42. </div>
  43. </section>
  44. <!-- Core Functions -->
  45. <section
  46. ref="functionsRef"
  47. class="section-container bg-#F6F8FD transition-all duration-1000 ease-out"
  48. :class="[isFunctionsVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-50px']"
  49. >
  50. <div class="landing-container lt-sm:px-32px">
  51. <h2 class="section-title pf-sc-semibold">{{ solution.coreFunctions.title }}</h2>
  52. <div class="grid grid-cols-1 sm:grid-cols-2 gap-24px mt-60px">
  53. <div
  54. v-for="(item, index) in solution.coreFunctions.function"
  55. :key="index"
  56. class="function-card"
  57. >
  58. <div class="function-card-header">
  59. <img src="~/assets/icons/function.svg" class="function-icon" alt="icon" />
  60. <h3 class="function-title pf-sc-semibold">{{ item.name }}</h3>
  61. </div>
  62. <p class="function-desc pf-sc-regular">{{ item.funcDesc }}</p>
  63. <div class="function-tags">
  64. <span
  65. v-for="(tag, tIndex) in item.funcPoints"
  66. :key="tIndex"
  67. class="function-tag pf-sc-medium"
  68. >
  69. <i class="i-custom-check-one wh-15px lt-sm:wh-24px"></i>
  70. {{ tag }}
  71. </span>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. </section>
  77. <!-- Core Effects -->
  78. <section
  79. ref="effectsRef"
  80. class="section-container bg-white transition-all duration-1000 ease-out"
  81. :class="[isEffectsVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-50px']"
  82. >
  83. <div class="landing-container">
  84. <h2 class="section-title pf-sc-semibold">{{ solution.coreEffects.title }}</h2>
  85. <div
  86. class="flex justify-between mt-80px gap-y-40px lt-sm:mt-72px lt-sm:grid lt-sm:grid-cols-2!"
  87. >
  88. <div
  89. v-for="(item, index) in solution.coreEffects.effect"
  90. :key="index"
  91. class="effect-item flex-1 text-center"
  92. >
  93. <div class="effect-percent d-din-pro-700-bold">{{ item.percent }}</div>
  94. <p class="effect-name pf-sc-regular">{{ item.name }}</p>
  95. </div>
  96. </div>
  97. </div>
  98. </section>
  99. <!-- Typical Cases -->
  100. <section
  101. v-if="currentCase"
  102. ref="casesRef"
  103. class="section-container solution-case transition-all duration-1000 ease-out"
  104. :class="[isCasesVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-50px']"
  105. >
  106. <div class="landing-container lt-sm:px-32px">
  107. <h2 class="section-title pf-sc-semibold">{{ solution.typicalCases.title }}</h2>
  108. <div class="mt-60px lt-sm:mt-48px">
  109. <solution-case-card
  110. :case-item="currentCase"
  111. :show-nav="(solution.typicalCases.cases?.length ?? 0) > 1"
  112. :is-first="activeCaseIndex === 0"
  113. :is-last="activeCaseIndex === (solution.typicalCases.cases?.length ?? 0) - 1"
  114. @prev="prevCase"
  115. @next="nextCase"
  116. />
  117. </div>
  118. </div>
  119. </section>
  120. <!-- CTA -->
  121. <section-cta />
  122. </div>
  123. </template>
  124. <script setup lang="ts">
  125. import { solutionPoints } from '@/constants/common'
  126. const route = useRoute()
  127. const { openConsultation } = useConsultation()
  128. const solution = computed(() => solutionPoints.find((item) => item.id === route.params.id))
  129. const painPointsRef = ref<HTMLElement | null>(null)
  130. const { isVisible: isPainPointsVisible } = useScrollReveal(painPointsRef)
  131. const functionsRef = ref<HTMLElement | null>(null)
  132. const { isVisible: isFunctionsVisible } = useScrollReveal(functionsRef)
  133. const effectsRef = ref<HTMLElement | null>(null)
  134. const { isVisible: isEffectsVisible } = useScrollReveal(effectsRef)
  135. const casesRef = ref<HTMLElement | null>(null)
  136. const { isVisible: isCasesVisible } = useScrollReveal(casesRef)
  137. const activeCaseIndex = ref(0)
  138. const currentCase = computed(() => {
  139. if (!solution.value?.typicalCases?.cases?.length) return null
  140. return solution.value.typicalCases.cases[activeCaseIndex.value]
  141. })
  142. const nextCase = () => {
  143. if (!solution.value?.typicalCases?.cases) return
  144. if (activeCaseIndex.value < solution.value.typicalCases.cases.length - 1) {
  145. activeCaseIndex.value++
  146. }
  147. }
  148. const prevCase = () => {
  149. if (activeCaseIndex.value > 0) {
  150. activeCaseIndex.value--
  151. }
  152. }
  153. useSeoMeta({
  154. title: () => `${solution.value?.title || '解决方案'} - 绘家科技`,
  155. description: () =>
  156. solution.value?.description ||
  157. '绘家科技致力于为物业管理公司提供数字化转型解决方案,提升服务品质与业主满意度。',
  158. keywords: () =>
  159. `${solution.value?.title},智慧物业,解决方案,绘家科技,${solution.value?.corePoints.points.map((p) => p.point).join(',')}`,
  160. ogTitle: () => `${solution.value?.title || '解决方案'} - 绘家科技`,
  161. ogDescription: () =>
  162. solution.value?.description ||
  163. '绘家科技致力于为物业管理公司提供数字化转型解决方案,提升服务品质与业主满意度。',
  164. })
  165. </script>
  166. <style scoped lang="scss">
  167. .section-container {
  168. @apply py-120px lt-sm:py-120px;
  169. }
  170. .section-title {
  171. @apply font-s-36px text-#000000 text-center lh-60px mb-40px lt-sm:font-s-48px lt-sm:mb-40px;
  172. }
  173. /* Hero */
  174. .solution-hero {
  175. @apply w-full h-600px relative bg-cover bg-center bg-no-repeat;
  176. @apply lt-sm:h-auto lt-sm:py-160px;
  177. background-image: url('~/assets/images/solution-bg.png');
  178. @screen lt-sm {
  179. background-image: url('~/assets/images/solution-mobile-bg.png');
  180. }
  181. }
  182. .hero-title {
  183. @apply font-s-48px text-#000000;
  184. @apply lt-sm:font-s-56px lt-sm:px-20px;
  185. }
  186. .hero-subtitle {
  187. @apply font-s-38px text-#000000 mb-16px;
  188. @apply lt-sm:font-s-38px lt-sm:px-20px;
  189. }
  190. .hero-desc {
  191. @apply font-s-18px text-#091221/70;
  192. @apply lt-sm:font-s-26px lt-sm:px-32px;
  193. }
  194. .hero-btn {
  195. @apply w-162px h-56px rounded-8px mt-36px font-s-18px text-white hover:opacity-80 transition-colors cursor-pointer;
  196. @apply lt-sm:w-224px lt-sm:h-71px lt-sm:rounded-8px lt-sm:font-s-28px;
  197. }
  198. /* Pain Points */
  199. .pain-card {
  200. @apply bg-white rounded-16px p-40px border border-#E2E8F0 shadow-sm transition-all duration-300 text-center flex flex-col items-center;
  201. @apply lt-sm:rounded-16px lt-sm:p-40px;
  202. }
  203. .pain-icon {
  204. @apply wh-64px mb-16px;
  205. @apply lt-sm:wh-69px;
  206. }
  207. .pain-title {
  208. @apply font-s-18px text-#091221 mb-16px;
  209. @apply lt-sm:font-s-32px;
  210. }
  211. .pain-desc {
  212. @apply font-s-14px text-#091221/70 text-center;
  213. @apply lt-sm:font-s-24px;
  214. }
  215. /* Functions */
  216. .function-card {
  217. @apply bg-gradient-to-b from-[#E5E9F5] to-[#EFF2FB] rounded-16px p-40px transition-all duration-300;
  218. @apply lt-sm:rounded-16px lt-sm:p-40px;
  219. }
  220. .function-card-header {
  221. @apply flex items-start flex-col gap-12px mb-12px;
  222. @apply lt-sm:gap-24px lt-sm:mb-24px;
  223. }
  224. .function-icon {
  225. @apply wh-44px;
  226. @apply lt-sm:wh-78px;
  227. }
  228. .function-title {
  229. @apply font-s-18px text-#091221;
  230. @apply lt-sm:font-s-32px;
  231. }
  232. .function-desc {
  233. @apply font-s-14px text-#091221/70 mb-12px lh-24px;
  234. @apply lt-sm:font-s-24px lt-sm:lh-normal lt-sm:mb-24px;
  235. }
  236. .function-tags {
  237. @apply flex flex-wrap gap-26px;
  238. @apply lt-sm:gap-24px;
  239. }
  240. .function-tag {
  241. @apply flex items-center font-s-14px text-#091221 gap-8px;
  242. @apply lt-sm:font-s-24px lt-sm:gap-8px;
  243. }
  244. /* Effects */
  245. .effect-percent {
  246. @apply font-s-48px text-#0F67F8 lt-sm:font-s-56px;
  247. }
  248. .effect-name {
  249. @apply font-s-16px text-#384146;
  250. }
  251. .solution-case {
  252. @apply bg-cover bg-center bg-no-repeat;
  253. background-image: url('~/assets/images/case-bg.png');
  254. }
  255. </style>